package robocup.component.worldobjects;

import java.util.EnumMap;

import robocup.component.SConf;
import robocup.component.actions.Action;
import robocup.component.actions.KickAction;
import robocup.component.actions.MoveAction;
import robocup.component.actions.TackleAction;
import robocup.component.geometry.Vektor;
import robocup.component.worldobjects.Player.RELATIVE;

/**
 * <code>Ball</code> - a world-model representation of the ball.
 */
public class Ball
    extends DynamicObject {

  /**
   * get a dummy instance the easy way.
   * 
   * @return a ball with no speed, null-position and no information, when it was
   *         seen last (-1)
   */
  public static Ball getDummyInstance() {

    return new Ball(-1, -1, new Vektor(), new Vektor());
  }

  /**
   * is set true, if a goalie catched the ball.
   */
  private boolean isCatched = false;

  public enum UPDATE_MODE {
    INIT,
    EXPECTED,
    HEARED,
    SEEN_FULL,
    SEEN_WITHOUT_SPEED,
    META_BASED_ON_INFO
  };

  private EnumMap<UPDATE_MODE, Integer> updateCycles          = new EnumMap<UPDATE_MODE, Integer>(
                                                                  UPDATE_MODE.class);
  private static final double[]         BALL_DEC_AFTER_N      = new double[50];
  private static final double[]         REL_BALL_DIST_AFTER_N = new double[50];

  /**
   * Constructs a ball-object with the given values
   * 
   * @param iCycle -
   *          when the ball was perceived last (real cycles)
   * @param iBodyCycle -
   *          when the ball was perceived last (in bodyCycles)
   * @param iPosAbs -
   *          the absolute ball position
   * @param iSpeed -
   *          the speed
   */
  public Ball(
      final int iCycle,
      final int iBodyCycle,
      final Vektor iPosAbs,
      final Vektor iSpeed) {

    super(iCycle, iBodyCycle, iPosAbs, iSpeed);
    Ball.BALL_DEC_AFTER_N[0] = 1;
    Ball.REL_BALL_DIST_AFTER_N[0] = 0;
    for (int i = 1; i < 50; i++) {
      Ball.BALL_DEC_AFTER_N[i] = Ball.BALL_DEC_AFTER_N[i - 1]
          * SConf.getInstance().BALL_DECAY;
      Ball.REL_BALL_DIST_AFTER_N[i] = Ball.REL_BALL_DIST_AFTER_N[i - 1]
          + Ball.BALL_DEC_AFTER_N[i - 1];
    }
  }

  /**
   * Clones the Ball.
   * 
   * @return The Ball clone
   */
  public Ball cloned() {

    final Ball b = Ball.getDummyInstance();
    b.copy(this);
    return b;
  }

  /**
   * clones a ball without creating new one.
   * 
   * @param b
   */
  public void copy(final Ball b) {

    this.setCycle(b.getCycle());
    this.setBodyCycle(b.getBodyCycle());
    this.setPosition(b.getPosition());
    this.setSpeed(b.getSpeed());
    this.setReliable(b.isReliable());
    this.setCatched(b.isCatched());
    this.setSpeedReliable(b.speedIsReliable());
    this.updateCycles = b.updateCycles.clone();
  }

  /**
   * sets the update mode. this is used to determine when the last update of a
   * certain update occured.
   * 
   * @param mode -
   *          the update mode
   * @param bodyCycle -
   *          the body cycle of the update
   */
  public void setUpdateMode(final UPDATE_MODE mode,
      final int bodyCycle) {

    this.updateCycles.put(mode, new Integer(bodyCycle));
  }

  /**
   * the age of the ball depending on the update type.
   * 
   * @param currentBodyCycle
   * @param mode
   * @return the age (wm.getBodyCycle - cycle of last update of given type)
   */
  public int getAge(final int currentBodyCycle,
      final UPDATE_MODE mode) {

    if (mode == UPDATE_MODE.META_BASED_ON_INFO) {
      return Math.min(this.getAge(currentBodyCycle,
          UPDATE_MODE.SEEN_WITHOUT_SPEED), this.getAge(currentBodyCycle,
          UPDATE_MODE.HEARED));
    }

    final Integer i = this.updateCycles.get(mode);
    if (i == null) {
      return Integer.MAX_VALUE;
    }

    if (mode == UPDATE_MODE.INIT) {
      return 0;
    }

    return currentBodyCycle - i.intValue();
  }

  /**
   * Predicts the effect of an action on the ball.
   * 
   * @param p -
   *          acting player
   * @param a -
   *          action
   * @return a clone of the ball after the action
   * 
   * @author rw
   */
  public Ball predictBallAfterAction(final Player p,
      final Action a) {

    final Ball b = this.cloned();
    b.predictThisAfterAction(p, a);
    return b;
  }

  /**
   * predicts the next ball based on an action. YOU ARE WORKING ON A REFERENCE!!
   * 
   * @param p -
   *          acting player
   * @param a -
   *          action
   * 
   * @author rw
   */
  public void predictThisAfterAction(final Player p,
      final Action a) {

    // ToDo check collision(rw)
    if (a == null) {
      this.extrapolate(1);
      return;
    }

    switch (a.getType()) {
    case MOVE:
      if (this.isCatched) {
        final MoveAction ma = (MoveAction) a;

        final Vektor next = new Vektor(Vektor.XY, ma.getX(), ma.getY());
        next.rotate(90);
        next.y = -next.y;
        this.setPosition(next);
        this.setSpeed(new Vektor(0, 0));
      }
      else {
        this.extrapolate(1);
      }
      break;

    case KICK:
      final KickAction kickAction = (KickAction) a;

      // relative position of ball
      final Vektor ballRel = p.getPosRel(this, Player.RELATIVE.TO_BODY);
      double ballDist = Math.abs(ballRel.getLength()
          - SConf.getInstance().BALL_SIZE
          - p.getPConf().PLAYER_SIZE);
      if (ballDist > p.getPConf().KICKABLE_MARGIN) {
        // it was not possible to kick the ball
        this.extrapolate(1);
        return;
      }
      if (ballDist < 0) {
        ballDist = 0;
      }
      // Equation 3.26 (page 29) from Master Thesis :o)
      final double kickedPower = kickAction.getPower()
          * (1.0 - 0.25 * Math.abs(ballRel.getAngle()) / 180.0 - 0.25
              * ballDist
              / p.getPConf().KICKABLE_MARGIN);
      // Equation 3.27 (page 29) from Master Thesis, ignoring the noise
      final Vektor ballAcceleration = new Vektor(Vektor.POLAR, kickedPower
          * SConf.getInstance().KICK_POWER_RATE, kickAction.getDirection()
          + p.getBodyDir());

      if (ballAcceleration.getLength() > SConf.getInstance().BALL_ACC_MAX) {
        ballAcceleration.setLength(SConf.getInstance().BALL_ACC_MAX);
      }

      // calculate new ball speed
      this.speed.addToThis(ballAcceleration);
      if (this.speed.getLength() > SConf.getInstance().BALL_SPEED_MAX) {
        this.speed.setLength(SConf.getInstance().BALL_SPEED_MAX);
      }

      this.position.addToThis(this.speed);
      // ball speed now decays (kick was performed in last cycle)
      this.speed.multThis(SConf.getInstance().BALL_DECAY);
      this.isCatched = false;
      return;

    case CATCH_BALL:
      this.isCatched = true;
      // TODO
      // couldn't take this code because
      // it was missing in the BallModel :(
      return;
    case TACKLE:

      final TackleAction tackleAction = (TackleAction) a;

      // relative position of ball
      final Vektor ballRel1 = p.getPosRel(this, Player.RELATIVE.TO_BODY);
      double ballDist1 = Math.abs(ballRel1.getLength()
          - SConf.getInstance().BALL_SIZE
          - p.getPConf().PLAYER_SIZE);
      // TODO tackledistance
      if (ballDist1 > SConf.getInstance().TACKLE_DIST) {
        // it was not possible to tackle the ball
        this.extrapolate(1);
        return;
      }
      if (ballDist1 < 0) {
        ballDist1 = 0;
      }
      // Changed to v12. getPower is calculated in a new manner
      // it depends on the given angle, which is the new param
      // for tackle (instead of power).
      double player_2_ball = p.getAngleRel(this, RELATIVE.TO_BODY);
      
      double tackledPower = tackleAction.getPower(player_2_ball);
      
      
      //eff_power *= 1.0 - 0.5*( std::fabs( player_2_ball.th() ) / M_PI );
      //tackleAction
      //tackledPower *=(1.0 - 0.5*(Math.toRadians()/Math.PI) );
      
      // Equation 3.27 (page 29) from Master Thesis, ignoring the noise
      final Vektor ballAcceleration1 = new Vektor(Vektor.POLAR, tackledPower,
          // * SConf.getInstance().TACKLE_POWER_RATE, 
    		  Vektor.normalize(tackleAction.getDirection() + p.getBodyDir()));
      //System.out.println("tp : " + tackledPower + " dri : " + tackleAction.getDirection() + p.getBodyDir() );
      //System.out.println("ballAccell : " + ballAcceleration1);
      if (ballAcceleration1.getLength() > SConf.getInstance().BALL_ACC_MAX) {
        ballAcceleration1.setLength(SConf.getInstance().BALL_ACC_MAX);
      }

      // calculate new ball speed
      this.speed.addToThis(ballAcceleration1);
      if (this.speed.getLength() > SConf.getInstance().BALL_SPEED_MAX) {
        this.speed.setLength(SConf.getInstance().BALL_SPEED_MAX);
      }

      this.position.addToThis(this.speed);
      // ball speed now decays (kick was performed in last cycle)
      this.speed.multThis(SConf.getInstance().BALL_DECAY);
      this.isCatched = false;
      return;
    default:
      this.extrapolate(1);
    }
  }

  /**
   * <code>isCatched</code>
   * 
   * @return true if a goalkeeper chatched the ball, else false.
   */
  public boolean isCatched() {

    return this.isCatched;
  } // end of method isCatched

  /**
   * <code>setCatched</code>
   */
  public void setCatched(final boolean chatched) {

    this.isCatched = chatched;
  } // end of method setChatched

  @Override
  public String toString() {

    return ("Ball C:"
        + this.getCycle()
        + " BC:"
        + this.getBodyCycle()
        + " Pos:"
        + this.getPosition()
        + " Speed:"
        + this.getSpeed()
        + " Reliable:" + this.isReliable());
  }

  /**
   * Extrapolates the Ball based on its speed.
   * 
   * @param n -
   *          horizont (cycles)
   * 
   * @author rw
   */
  public void extrapolate(final int n) {

    // as profiling shows that Math.pow is quiet slow
    // the common case of extrapoling by 1 cycle
    // is handled in a more efficient way
    if (n == 0)
      return;
    if (n == 1) {
      this.position.x += this.speed.x;
      this.position.y += this.speed.y;
      this.speed.x *= SConf.getInstance().BALL_DECAY;
      this.speed.y *= SConf.getInstance().BALL_DECAY;
    }
    else {
      this.position.x += this.speed.x
          * Ball.relativeBallMovementAfterNCycles(n);
      this.position.y += this.speed.y
          * Ball.relativeBallMovementAfterNCycles(n);
      this.speed.x *= Ball.ballDecayAfterNCycles(n);
      this.speed.y *= Ball.ballDecayAfterNCycles(n);
    }
    // no stamina update as the main actions are missing
  }

  /**
   * Calculates the decay that is used to calculate the ball speed after n
   * cycles. (Equals Math.pow(ball_dec,n) but is faster)
   * 
   * @param n
   *          the number of cycles
   * @return the decay for n cycles
   */
  public static double ballDecayAfterNCycles(final int n) {

    if (n < 0) {
      throw new IllegalArgumentException("no ball decay for " + n + " cycles");
    }
    if (n == 0) {
      return 1;
    }
    if (n >= 50) {
      return ballDecayAfterNCycles(n - 1) * SConf.getInstance().BALL_DECAY;
    }
    return BALL_DEC_AFTER_N[n];
  }

  /**
   * Calculates the ball movement after n cycles starting with speed 1. So for
   * cycle 0 there is no movement. For cycle 1 the movement is 1 and that
   * continues in a geometric row.
   * 
   * @param n
   *          the number of cycles
   * @return the relative ball movement after n cycles
   */
  public static double relativeBallMovementAfterNCycles(final int n) {

    if (n < 0) {
      throw new IllegalArgumentException("no ball movement for "
          + n
          + " cycles");
    }
    if (n >= 50) {
      return relativeBallMovementAfterNCycles(n - 1)
          + ballDecayAfterNCycles(n - 1);
    }
    return REL_BALL_DIST_AFTER_N[n];
  }
}
